# 其它面试题
# 为什么选择使用框架而不是原生?
组件化: 其中以 React 的组件化最为彻底,甚至可以到函数级别的原子组件,高度的组件化可以是我们的工程易于维护、易于组合拓展。
天然分层: JQuery 时代的代码大部分情况下是面条代码,耦合严重,现代框架不管是 MVC、MVP还是MVVM 模式都能帮助我们进行分层,代码解耦更易于读写。
生态: 现在主流前端框架都自带生态,不管是数据流管理架构还是 UI 库都有成熟的解决方案。
开发效率: 现代前端框架都默认自动更新DOM,而非我们手动操作,解放了开发者的手动DOM成本,提高开发效率,从根本上解决了UI 与状态同步问题.
# Hybrid如何通信的?
- API 注入,原理其实就是 Native 获取 JavaScript环境上下文,并直接在上面挂载对象或者方法,使 js 可以直接调用,Android 与 IOS 分别拥有对应的挂载方式'
- WebView 中的 prompt/console/alert 拦截,通常使用 prompt,因为这个方法在前端中使用频率低,比较不会出现冲突
- WebView URL Scheme 跳转拦截
# 知道npm ci么,和npm install的区别是啥?
npm install 读取 package.json 以创建依赖关系列表,并使用 package-lock.json 告知要安装这些依赖关系的版本。如果依赖项不在 package-lock.json 中,它将由 npm install 添加。
npm ci(以持续集成命名)直接从 package-lock.json 安装依赖关系,并且仅使用 package.json 来验证没有不匹配的版本。如果缺少任何依赖项或版本不兼容,则将引发错误。
速度上 ci 明显比 install 快,线上发布打包的时候使用 ci 是优于 install 的
# Gzip原理是什么?
GZIP 的核心是 Deflate,在 RFC 1951 中被标准化,并且在当时作为 LZW 的替代品有了非常广泛的使用。
Deflate 是一个同时使用 LZ77 与 Huffman Coding 的算法
# nginx 负载均衡的算法是什么?
通过负载均衡充利用服务器资源,nginx 目前支持自带 4 种负载均衡策略,还有 2 种常用的第三方策略:
- 轮询策略(默认)
- 根据服务器权重
- 客户端 ip 绑定(ip_hash)
- 最小连接数策略
- 最快响应时间策略(依赖于第三方 NGINX Plus)
- 按访问 url 的 hash 结果(第三方)
# 页面10张img,http1是怎样的加载表现,怎么解决。那多域名又为什么可以解决呢?
http1下,浏览器对一个域名下最大tcp连接数为6,所以10张图片表现为6+4。所以可以用多域名部署解决。5个a域名,5个b域名就可以实现一瞬间全部出来了(或者6个a,4个b,融会贯通)。如果是1个a域名,9个多域名,会表现为(6 + 1) + 3
# ttp缓存是怎样的。etag和last modify分别什么优点缺点,适合什么场景【描述】【举例】
缓存、304基本问题。etag适合重要量小的资源,last modify适合不重要的量大的资源。注意last modify需要保证服务器时间准确。
待完善。。。
# jsbridge了解么,说一下【举例】
# common.js和 esm 区别
- CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用。
- CommonJS 模块是运行时加载,ES6 模块是编译时输出接口。
- CommonJS 模块的require()是同步加载模块,ES6 模块的import命令是异步加载,有一个独立的模块依赖的解析阶段。
es6模块与CommonJS模块的差异 (opens new window)
前端模块化——彻底搞懂AMD、CMD、ESM和CommonJS (opens new window)
# 完全不同的域名共享localstorage的方案【描述】
公共服务器双工通信(比较简单暴力)、嵌套iframe并双向通信(只要一个变了,马上通知另一方,保证他们的localstorage一模一样)
# 一次最稳妥的git上传代码的流程是什么
- git stash (这是将本地代码回滚值至上一次提交的时候,就是没有你新改的代码)
- git pull origin master(将远程的拉下来)
- git stash pop(将第一步回滚的代码释放出来,相等于将你修改的代码与下拉的代码合并)然后解决冲突,你本地的代码将会是最新的代码
- git add .
- git commit -m""
- git push origin master 这几步将代码推至了远程
- 最后再git pull origin master 一下,确保远程的全部拉下来,有的你刚提交完有人又提交了,你再拉一下会避免比的不是最新的问题
# 移动端适配方案具体实现以及对比
常见的移动端适配方案:
- media queries
- flex 布局
- rem + viewport
- vh vw
- 百分比
一、Meida Queries
meida queries 的方式可以说是我早期采用的布局方式,它主要是通过查询设备的宽度来执行不同的 css 代码,最终达到界面的配置。
核心语法:
@media only screen and (max-width: 374px) {
/* iphone5 或者更小的尺寸,以 iphone5 的宽度(320px)比例设置样式*/
}
@media only screen and (min-width: 375px) and (max-width: 413px) {
/* iphone6/7/8 和 iphone x */
}
@media only screen and (min-width: 414px) {
/* iphone6p 或者更大的尺寸,以 iphone6p 的宽度(414px)比例设置样式 */
}
2
3
4
5
6
7
8
9
优点:
- media query 可以做到设备像素比的判断,方法简单,成本低,特别是针对移动端和 PC 端维护同一套代码的时候。目前像 Bootstrap 等框架使用这种方式布局
- 图片便于修改,只需修改 css 文件
- 调整屏幕宽度的时候不用刷新页面即可响应式展示
缺点:
- 代码量比较大,维护不方便
- 为了兼顾大屏幕或高清设备,会造成其他设备资源浪费,特别是加载图片资源
- 为了兼顾移动端和 PC 端各自响应式的展示效果,难免会损失各自特有的交互方式
二、Flex 弹性布局
以天猫的实现方式进行说明:
它的 viewport 是固定的:<meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no">
高度定死,宽度自适应,元素都采用 px 做单位。
随着屏幕宽度变化,页面也会跟着变化,效果就和 PC 页面的流体布局差不多,在哪个宽度需要调整的时候使用响应式布局调调就行(比如网易新闻),这样就实现了『适配』。
三、rem+viewport 缩放
实现原理:
根据 rem 将页面放大 dpr 倍, 然后 viewport 设置为 1/dpr.
- 如 iphone6 plus 的 dpr 为 3, 则页面整体放大 3 倍, 1px(css 单位)在 plus 下默认为 3px(物理像素)
- 然后 viewport 设置为 1/3, 这样页面整体缩回原始大小. 从而实现高清。
这样整个网页在设备内显示时的页面宽度就会等于设备逻辑像素大小,也就是 device-width。这个 device-width 的计算公式为:
设备的物理分辨率/(devicePixelRatio * scale),在 scale 为 1 的情况下,device-width = 设备的物理分辨率/devicePixelRatio。
四、rem 实现
rem 是相对长度单位,rem 方案中的样式设计为相对于根元素 font-size 计算值的倍数。根据屏幕宽度设置 html 标签的 font-size,在布局时使用 rem 单位布局,达到自适应的目的。
viewport 是固定的:<meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no">
。
可以使用淘宝的 flexible.js 来处理根元素的自动计算。
优点:
- 兼容性好,页面不会因为伸缩发生变形,自适应效果更佳。
缺点:
- 不是纯 css 移动适配方案,需要在头部内嵌一段 js脚本监听分辨率的变化来动态改变根元素的字体大小,css样式和 js 代码有一定耦合性,并且必须将改变font-size的代码放在 css 样式之前。
- 小数像素问题,浏览器渲染最小的单位是像素,元素根据屏幕宽度自适应,通过 rem 计算后可能会出现小数像素,浏览器会对这部分小数四舍五入,按照整数渲染,有可能没那么准确。
五、纯 vw 方案
视口是浏览器中用于呈现网页的区域。
- vw : 1vw 等于 视口宽度 的 1%
- vh : 1vh 等于 视口高度 的 **1% **
- vmin : 选取 vw 和 vh 中 最小 的那个
- vmax : 选取 vw 和 vh 中 最大 的那个
虽然 vw 能更优雅的适配,但是还是有点小问题,就是宽,高没法限制。
$base_vw = 375;
@function vw ($px) {
return ($px/$base_vw) * 100vw
};
2
3
4
优点:
- 纯 css 移动端适配方案,不存在脚本依赖问题。
- 相对于 rem 以根元素字体大小的倍数定义元素大小,逻辑清晰简单。
缺点:
- 存在一些兼容性问题,有些浏览器不支持
六、vw + rem 方案
// scss 语法
// 设置html根元素的大小 750px->75 640px->64
// 将屏幕分成10份,每份作为根元素的大小。
$vw_fontsize: 75
@function rem($px) {
// 例如:一个div的宽度为100px,那么它对应的rem单位就是(100/根元素的大小)* 1rem
@return ($px / $vw_fontsize) * 1rem;
}
$base_design: 750
html {
// rem与vw相关联
font-size: ($vw_fontsize / ($base_design / 2)) * 100vw;
// 同时,通过Media Queries 限制根元素最大最小值
@media screen and (max-width: 320px) {
font-size: 64px;
}
@media screen and (min-width: 540px) {
font-size: 108px;
}
}
// body 也增加最大最小宽度限制,避免默认100%宽度的 block 元素跟随 body 而过大过小
body {
max-width: 540px;
min-width: 320px;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
七、百分比
使用百分比%定义宽度,高度用px固定,根据可视区域实时尺寸进行调整,尽可能适应各种分辨率,通常使用 max-width/min-width 控制尺寸范围过大或者过小。
优点:
- 原理简单,不存在兼容性问题
缺点:
- 如果屏幕尺度跨度太大,相对设计稿过大或者过小的屏幕不能正常显示,在大屏手机或横竖屏切换场景下可能会导致页面元素被拉伸变形,字体大小无法随屏幕大小发生变化。
- 设置盒模型的不同属性时,其百分比设置的参考元素不唯一,容易使布局问题变得复杂。
# 响应式背后的浏览器原理你知道吗?
根据浏览器或设备的分辨率可以计算获取到相应的尺寸,通过不同的尺寸可以动态的修改 html 元素或者盒子在浏览器中的大小,从而实现响应式。
# 组件库设计有什么原则?
- 标准性
- 独立性
- 复用与易用
- 适用SPOT法则
- 避免暴露组件内部实现
- 避免直接操作DOM,避免使用ref
- 入口处检查参数的有效性,出口处检查返回的正确性
- 无环依赖原则(ADP)
- 稳定抽象原则(SAP)
- 避免冗余状态
- 合理的依赖关系
- 扁平化参数
- 良好的接口设计
- API尽量和已知概念保持一致
# git rebase 和 git merge 的区别
git merge 和 git rebase 都是用于分支合并,关键在 commit 记录的处理上不同。
git merge 会新建一个新的 commit 对象,然后两个分支以前的 commit 记录都指向这个新 commit 记录。这种方法会保留之前每个分支的 commit 历史。
git rebase 会先找到两个分支的第一个共同的 commit 祖先记录,然后将提取当前分支这之后的所有 commit 记录,然后将这个 commit 记录添加到目标分支的最新提交后面。经过这个合并后,两个分支合并后的 commit 记录就变为了线性的记录了。
# CDN 的工作原理
CDN网络是在用户和服务器之间增加Cache层,主要是通过接管DNS实现,将用户的请求引导到Cache上获得源服务器的数据,从而降低网络的访问时间。
# 简述浏览器的垃圾回收机制
V8 实现了准确式 GC,GC 算法采用了分代式垃圾回收机制。因此,V8 将内存(堆)分为新生代和老生代两部分。
# common.js和ES6模块怎么解决模块化循环引用问题?
CommonJS模块:
使用require语句导入模块,module.exports导出模块,输出的是值的拷贝,按需引入,同步执行
对于基本数据类型,属于复制,对于复杂数据类型,属于浅拷贝
如何解决循环加载的原理:循环加载时,属于加载时执行。即脚本代码在require的时候,就会全部执行,然后在内存中生成该模块的一个说明对象。当再次执行require命令,下次会直接读取缓存。一旦出现某个模块被"循环加载",就只输出已经执行的部分,还未执行的部分不会输出(解决原理)
ES6模块:
import语句导入模块,export语句导出模块,是异步的
ES6模块原理:不论是基本数据类型还是复杂数据类型。当模块遇到import命令时,就会生成一个只读引用,脚本真正执行时,再根据这个只读引用,到被加载的那个模块里面去取值。循环加载时,ES6模块是动态引用。只要两个模块之间存在某个引用,代码就能够执行(解决原理)
# 你是怎么实现前端自动部署的?
公司项目使用了 gitlab 来管理代码 和 Jenkins 来集成构建代码。可以在 gitlab 中配置 commit 钩子,commit 自动触发 Jenkins 构建,构建脚本在 Jenkins 中配置。
补充:
如果没有用 Jenkins,也可以在配置 gitlab 的 commit 钩子函数后,通过配置 gotlab 的 .gitlab-ci.yml 文件来实现自动部署。
我还使用过 scp2 库来创建过自动部署脚本。scp2 是一个基于 ssh2 增强实现,纯粹使用 JavaScript 编写。而 ssh2 就是一个使用 nodejs 对于 SSH2 的模拟实现。scp,是 secure copy 的缩写, scp 是 Linux 系统下基于 SSH 登陆进行安全的远程文件拷贝命令。这里我们就用这个功能,在 Vue 编译构建成功之后,将项目推送至测试/生产环境,以方便测试,提高效率。
其它还知道方法:
可以利用 webpack 插件,在 Webpack 即将退出时再附加一些额外的操作
要实现该插件,需要借助两个事件:
- done:在成功构建并且输出了文件后,Webpack 即将退出时发生;
- failed:在构建出现异常导致构建失败,Webpack 即将退出时发生;
在 apply 方法中监听这两个事件即可:
class EndWebpackPlugin {
constructor(doneCallback, failCallback) {
// 存下在构造函数中传入的回调函数
this.doneCallback = doneCallback;
this.failCallback = failCallback;
}
apply(compiler) {
compiler.plugin('done', (stats) => {
// 在 done 事件中回调 doneCallback
this.doneCallback(stats);
});
compiler.plugin('failed', (err) => {
// 在 failed 事件中回调 failCallback
this.failCallback(err);
});
}
}
// 导出插件
module.exports = EndWebpackPlugin;
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 你是怎么实现前端监控的?(错误监控,性能监控)
错误监控
性能监控
利用 performance API let timing = performance.getEntriesByType('navigation')[0];
或者 let timing = performance.timing
。封装一个函数,在页面加载完毕后执行,做了一些各个阶段性能指标的计算,然后通过接口发送到服务器,用于统计判断,主要监控了这 6 个时间:
DNS 解析耗时: domainLookupEnd - domainLookupStart
TCP 连接耗时: connectEnd - connectStart
SSL 安全连接耗时: connectEnd - secureConnectionStart
TTFB 首次网络请求耗时: responseStart - requestStart
白屏时间: responseEnd - fetchStart
首次可交互时间: domInteractive - fetchStart
2
3
4
5
6
注:这些问题最好都根据自己的实际情况回答~
# 项目中有哪些难点,怎么解决的?
注:这些问题最好都根据自己的实际情况回答~
前端方面
遇到客服反馈个别用户加载不出来我们的网站部分页面。
当时遇到这个问题很头疼,因为我们各个浏览器都测试过了,都是好的,也让测试测试了好多遍,就是没有找到问题所在。
然后刚开始项目没有用前端监控,但是有埋点,我就想到通过埋点记录用户的行为,判断用户到了哪一步出问题,并且有问题的页面加载情况也记录下来。
看埋点记录发现,出问题的用户只要访问我们的广告管理相关页面就会出现页面空白或者加载不出来。
然后我去排查了好多次代码,还是没有发现为什么,最后想起浏览器插件可能会对网站产生影响,所以就去查了一下发现 chrome 屏蔽广告的插件会影响网站页面的加载和内容
当时马上让客服通知用户暂时把插件关闭了,发现果然可以访问了。然后就让用户把用的插件告诉了我,我拿来做测试,修改项目里面会影响加载的广告字段。
这件事也是后面推进前端错误监控的原因之一
后端方面
测试在测试的时候,发现一台java服务进程异常退出,其实一周前已经发生过一次了,但是上次没在意,以为是有人登录不小心关闭了。可是这次又出现了,说明这是一个问题,而不是偶然现象。我登录上去排查,通过查阅 /var/log/messages
日志,可以看到是因为操作系统内存耗尽,触发了OOM killer
,杀死了A服务。并且我们同时也看到了上周日也是因为触发了 OOM killer
,导致进程被 OS 杀。
从日志中我们可以看到,A 服务占用了操作系统大量的内存导致无法给其他应用程序分配内存。当时第一直觉是 A 服务发生了内存泄漏(后来证明第一直觉是错的)。关于内存泄露的文章网上有一大堆,简而言之就是 JVM 里创建了大量的无法被垃圾回收但是不再被使用的对象。
我先去大概排查了一下代码,暂时没有发现明显的内存泄漏,主要排查了一下:
- 静态的集合类:由于这些变量的生命周期与应用程序保持一致,所以会导致这些类中的对象就算被手动置为null,也无法被释放。
- 单例:同1,单例类的生命周期与应用程序也保持一致,如果单例类中也保持了某些对象,那么这些对象也是无法被回收的。
- 各种连接、IO:这些操作都是十分消耗资源的,如果忘记了关闭,比如忘记关闭数据库连接,那么也是会发生内存泄漏的情况。
- threadlocal:由于threadlocal变量的生命周期与线程保持一致,当与线程池结合使用时,由于线程使用完毕后不会销毁,所以threadlocal变量可能也会出现泄漏的情况。
- 其他:比如监听器,外部模块调用等。。
大体上所有的内存泄露的原因都可以通过研究 Heap Dump 文件(堆转储)来解决。JVM 可以通过 jmap 命令 dump 出当前的 heap 快照,通常后缀名为 .hprof
。JVM 支持在 OOM 时,dump 出当前的 heap,便于开发人员分析为何出现了 OOM。
看了一下我们当前的启动脚本,发现没有添加导出 HeapDump 文件的参数。所以在所有的启动语句中加上了 -XX:+HeapDumpOnOutOfMemoryError
,当 JVM 发生 OOM 时,可以自动地导出 HeapDump 文件。但是发现该参数没有起作用,经过仔细研究之后发现, 操作系统层级的 OOM killer
是不会触发 HeapDumpOnOutOfMemoryError
,因为 OOM killer
直接杀死JVM进程,不会给你JVM任何机会,比如导出Heap,或者执行一些其他hook命令。但是当时并没有意识到这一点,只是接着往下走,也因此走了很多弯路。
定位内存泄露的核心思路是通过两份及以上的快照比对,查看是否有某些对象一直在增多,如果有的话,那么很可能发生了内存泄漏 。为了研究 hprof 文件,我们需要借助 MAT(Eclipse Memory Analyzer Tool)。该工具的用法十分简单,照着网上的教程学一下就会。
到了这一步,我开始怀疑其实A服务没有内存泄露的代码。那么如果没有内存泄露的话,为什么会占用这么高的内存呢?其实这个问题仔细想想,答案很简单,但是当时方向还是错了,我觉得还是有哪些地方可能出现了内存泄漏,所以上了 profile 工具。java 世界中的 profile 工具首推JProfiler,网上可以下载破解版(付费的实在是太贵了)。通过JProfiler,我们可以实时监测 JVM 的一举一动。该工具十分好用,并且界面做的十分好看,推荐大家使用。PS:其实 JProfiler 包含了 MAT 的所有功能,只是 MAT 更加简洁,而且还免费。
A服务其实没有存在内存泄漏。回过头来看,其实我们的OOM发生在操作系统层,和我们平时遇到的 JVM 的 OOM 是不一样的。这里需要介绍一下OOM killer:操作系统在给应用程序分配内存时,采用的是over-commit策略,即无论应用需要多少内存,操作系统都会允许,即使超过了目前剩余的容量,操作系统依然允许。这是因为操作系统觉得大部分的应用程序并不会用满他自己声明的内存。但是还是可能会出现大家都用满了自己声明的内存的情况,这个时候操作系统如果不释放内存出来,很可能连自己都无法运行了,所以这个时候操作系统就会叫 OOM killer 过来,给所有的进程打分,选出得分最高者,直接 kill 掉。一般都是内存占用最高的进程被 kill 掉。
那么A服务为什么会占用这么多内存呢?当我们排除了“内存泄露”这个原因,那么只可能是A服务确实占用了这么多内存。通过 java -XX:+PrintFlagsFinal -version | grep -iE 'HeapSize|PermSize|ThreadStackSize'
,我们可以看到A服务的测试机上的默认JVM配置:
我们可以看到,在这台内存仅有1G的服务器上,MaxHeapSize竟然达到了500兆。由于A服务的不断运行,迟早有一天会吃满整个HeapSize,由于JVM不会自动归还HeapSize,就算你实际使用的heap大小只有一点点,操作系统还是认为你占用了这么多的HeapSize。所以这就解释了为什么我们需要一周的时间才会发生这个场景,那是因为由初始的HeapSize达到MaxHeapSize需要长时间的运行以积累足够的对象触发JVM的Heap扩展策略。也解释了在每次进程消失的时间点,我们实际上没有做什么消耗资源的操作,但进程还是退出了,那是因为操作系统只认你占领的内存空间,并不会管你实际使用了多少空间。
具体的解决方案:
- 测试环境默认创建的线程数以及连接数需要往小设,因为测试环境不会有这么多的并发。
- 手动配置JVM的最大堆(-Xmx)和初始堆(-Xms)的大小,具体大小值需要根据实际情况而定,设置得太大容易被 OOM killer 杀掉,设置地太小容易发生十分频繁的GC操作。在测试环境中,可以尽量往小配置,避免其他应用影响。
- 加上
-XX:+HeapDumpOnOutOfMemoryError
,便于在JVM发生OOM时,可以还原“事故现场”。 - supervisor 进程守护
# 未来的一些方向
PWA
- 渐进式增强WEB应用, 是Google 在2016年提出的概念,2017年落地的web技术。目的就是在移动端利用提供的标准化框架,在网页应用中实现和原生应用相近的用户体验的渐进式网页应用。
跨端
- RN ⽣态已经⾮常成熟,或者说看不到太多发展前景,因为目前还停留在0.61版本,似乎1.0版本仍然遥遥无期
- ⾕歌⽣态的 Flutter,特别是 Flutter for Web 的第⼀个 Release,⼜让 Web 前端重燃希望、跃跃欲试。
serverless(无服务架构)
- Serverless 的⽕爆⼏乎可以归因于前端。因为 Serverless 能够较完美的⽀持 Node.js
- 随着 Node.js 成为前端开发者必备技能之后,云计算的不断普及会让Serverless 触⼿可及。当越来越多的开发者尝到研发⾼效的甜头之后,Serverless 必将对前端的研发模式产⽣变⾰。
微前端
- 个人认为很难普及,小公司很难使用上
webassemblely
- WebAssembly 并不是一门编程语言,而是一份字节码标准,需要用高级编程语言编译出字节码放到 WebAssembly 虚拟机中才能运行, 浏览器厂商需要做的就是根据 WebAssembly 规范实现虚拟机
- 有了 WebAssembly,在浏览器上可以跑任何语言。从 Coffee 到 TypeScript,到 Babel,这些都是需要转译为 js 才能被执行的,而 WebAssembly 是在浏览器里嵌入 vm,直接执行,不需要转译,执行效率自然高得多。